package se.lnu.axez;

import android.opengl.Matrix;

/**
 * Represents local transformation in 3D space.
 * 
 * @author stan
 * 
 */
public final class Transformation {
	/**
	 * Offset of translation values x, y, z within values array.
	 */
	private static final int TRANSLATION_OFFSET = 0;

	/**
	 * Offset of rotation values a, x, y, z within values array.
	 */
	private static final int ROTATION_OFFSET = TRANSLATION_OFFSET + 3;

	/**
	 * Offset of uniform scale value within values array.
	 */
	private static final int SCALE_OFFSET = ROTATION_OFFSET + 4;

	/**
	 * Offset of precalculated OpenGL transformation matrix within values array.
	 */
	private static final int TRANSFORM_MATRIX_OFFSET = SCALE_OFFSET + 3;

	/**
	 * Size of values array.
	 */
	private static final int VALUES_ARRAY_SIZE = TRANSFORM_MATRIX_OFFSET
			+ Constants.GL_MATRIX_SIZE;

	/**
	 * Values array storing all transformation values and precalculated OpenGL
	 * transformation matrix.
	 */
	private final float[] values = new float[VALUES_ARRAY_SIZE];

	/**
	 * Triple to return translation.
	 */
	private final FloatTriple translation = new FloatTriple(this.values,
			TRANSLATION_OFFSET, TRANSLATION_OFFSET + 1, TRANSLATION_OFFSET + 2);

	/**
	 * Quad to return rotation.
	 */
	private final FloatQuad rotation = new FloatQuad(this.values,
			ROTATION_OFFSET + 1, ROTATION_OFFSET + 2, ROTATION_OFFSET + 3,
			ROTATION_OFFSET);

	/**
	 * Indicates whether the instance was updated and OpenGL transformation
	 * matrix recalculation is required.
	 */
	private boolean needMatrixRecalc = false;

	/**
	 * Instantiates new {@link Transformation}.
	 */
	Transformation() {
		Matrix.setIdentityM(this.values, TRANSFORM_MATRIX_OFFSET);
	}
	
	/**
	 * Gets local translation.
	 */
	public FloatTriple getTranslation() {
		return this.translation;
	}

	/**
	 * Sets local translation.
	 * 
	 * @return self.
	 */
	public Transformation setTranslation(float x, float y, float z) {
		this.values[TRANSLATION_OFFSET] = x;
		this.values[TRANSLATION_OFFSET + 1] = y;
		this.values[TRANSLATION_OFFSET + 2] = z;
		this.needMatrixRecalc = true;
		return this;
	}
	
	public Transformation setXTranslation(float x) {
		this.values[TRANSLATION_OFFSET] = x;
		this.needMatrixRecalc = true;
		return this;
	}
	
	public Transformation setYTranslation(float y) {
		this.values[TRANSLATION_OFFSET + 1] = y;
		this.needMatrixRecalc = true;
		return this;
	}

	public Transformation setZTranslation(float z) {
		this.values[TRANSLATION_OFFSET + 2] = z;
		this.needMatrixRecalc = true;
		return this;
	}
	
	/**
	 * Gets local uniform scale. 1f is default scale.
	 */
	public float getUniformScale() {
		return this.values[SCALE_OFFSET];
	}
	
	/**
	 * Sets local uniform scale.
	 */
	public Transformation setUniformScale(float uniformScale) {
		return this.setScale(uniformScale, uniformScale, uniformScale);
	}

	/**
	 * Sets local non-uniform scale.
	 */
	public Transformation setScale(float xScale, float yScale, float zScale) {
		this.values[SCALE_OFFSET] = xScale;
		this.values[SCALE_OFFSET + 1] = yScale;
		this.values[SCALE_OFFSET + 2] = zScale;
		this.needMatrixRecalc = true;
		return this;
	}

	/**
	 * Sets local rotation. Each axis has a multiplication factor defining how
	 * much control is rotated around specifica axis. Factor equal to 0f means
	 * no rotation around the axis, whereas 1f means full angle rotation.
	 * 
	 * @param angle
	 *            - rotation angle in degrees.
	 * @param xFactor
	 *            - multiplication factor for x axis.
	 * @param yFactor
	 *            - multiplication factor for y axis.
	 * @param zFactor
	 *            - multiplication factor for z axis.
	 */
	public Transformation setRotation(float angle, float xFactor, float yFactor,
			float zFactor) {
		this.values[ROTATION_OFFSET] = angle;
		this.values[ROTATION_OFFSET + 1] = xFactor;
		this.values[ROTATION_OFFSET + 2] = yFactor;
		this.values[ROTATION_OFFSET + 3] = zFactor;
		this.needMatrixRecalc = true;
		return this;
	}

	public synchronized Transformation setOrientation(float[] orientationMatrix, int offset) {
		this.needMatrixRecalc = false;
		System.arraycopy(orientationMatrix, offset, this.values, TRANSFORM_MATRIX_OFFSET, Constants.GL_MATRIX_SIZE);
		return this;
	}
	
	/**
	 * Returns local rotation.
	 * 
	 * @return quad with rotation angle and factors.
	 */
	public FloatQuad getRotation() {
		return this.rotation;
	}

	/**
	 * Applies current transformation matrix to active model-view matrix within
	 * {@link RenderMatrices} instance.
	 * 
	 * @param rm
	 *            - {@link RenderMatrices} instance to be updated.
	 */
	void applyModelViewMatrix(RenderMatrices rm) {
		this.recalculateMatrix();
		rm.multiplyModelViewMatrix(this.values, TRANSFORM_MATRIX_OFFSET);
	}
	
	public void copyTransformMatrix(float[] matrixDestination, int offset) {
		this.recalculateMatrix();
		System.arraycopy(this.values, TRANSFORM_MATRIX_OFFSET, matrixDestination, offset, Constants.GL_MATRIX_SIZE);
	}
	
	/**
	 * Recalculates OpenGL transform matrix.
	 */
	private synchronized void recalculateMatrix() {
		if (!this.needMatrixRecalc) {
			return;
		}
		
		Matrix.setIdentityM(this.values, TRANSFORM_MATRIX_OFFSET);
		float xScale = this.values[SCALE_OFFSET];
		float yScale = this.values[SCALE_OFFSET + 1];
		float zScale = this.values[SCALE_OFFSET + 2];
		if (xScale != 0f && yScale != 0f && zScale != 0f) {
			Matrix.scaleM(this.values, TRANSFORM_MATRIX_OFFSET, xScale, yScale, zScale);
		}
		
		Matrix.translateM(this.values, TRANSFORM_MATRIX_OFFSET, 
				this.values[TRANSLATION_OFFSET],
				this.values[TRANSLATION_OFFSET + 1],
				this.values[TRANSLATION_OFFSET + 2]);
		
		float angle = this.values[ROTATION_OFFSET];
		if (angle != 0) {
			Matrix.rotateM(this.values, TRANSFORM_MATRIX_OFFSET,
					angle,
					this.values[ROTATION_OFFSET + 1],
					this.values[ROTATION_OFFSET + 2],
					this.values[ROTATION_OFFSET + 3]);
		}
		
		this.needMatrixRecalc = false;
	}
	
	@Override
	public String toString() {
		this.recalculateMatrix();
		
		StringBuilder sb = new StringBuilder();
		String lineSeparator = System.getProperty("line.separator");
		sb.append("[");
		
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 1]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 2]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 3]).append(lineSeparator);
		
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 4]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 5]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 6]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 7]).append(lineSeparator);
		
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 8]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 9]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 10]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 11]).append(lineSeparator);

		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 12]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 13]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 14]).append(", ");
		sb.append(this.values[TRANSFORM_MATRIX_OFFSET + 15]).append("]");

		return sb.toString();
	}
}
